<?php
/**
 * Project: AIGC
 * Author: moufer <moufer@163.com>
 * Date: 2024/7/13 11:09
 */

namespace module\ai\library\channel\abstract;

use Generator;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use module\ai\library\channel\exception\CompletionException;
use module\ai\library\channel\interface\CompletionInterface;
use module\ai\library\channel\interface\LlmInterface;
use module\ai\model\Channel;
use module\ai\model\Key;
use Psr\Http\Message\ResponseInterface;

abstract class Llm implements LlmInterface
{
    /**
     * 接口基础地址
     * @var string
     */
    protected string $baseUrl = '';

    /**
     * @var array 配置信息
     */
    protected array $config = [];

    /**
     * @var bool 流式回答
     */
    protected bool $isStream = true;

    /**
     * @var string 应答系统信息
     */
    protected string $system = '';

    /**
     * 提交参数
     * @var array
     */
    protected array $payload = [];

    /**
     * 密钥
     * @var Key
     */
    protected Key $keyModel;

    /**
     * @param Channel $channel AI通道
     */
    public function __construct(protected Channel $channel)
    {
        $this->setPayload($channel->config);
    }

    /**
     * 设置为流式回答
     * @param bool $isStream
     * @return $this
     */
    public function withStream(bool $isStream = true): static
    {
        $this->isStream = $isStream;
        return $this;
    }

    /**
     * 设置系统信息
     * @param string $content
     * @return Llm
     */
    public function withSystem(string $content): static
    {
        $this->system = $content;
        return $this;
    }

    /**
     * 设置回答模型
     * @param string $model
     * @return $this
     */
    public function withModel(string $model): static
    {
        $this->payload['model'] = $model;
        return $this;
    }

    /**
     * 添加历史记录
     * @param $prompt
     * @param $response
     * @return $this
     */
    public function withHistory($prompt, $response): static
    {
        return $this->withMessage($prompt, 'user')
            ->withMessage($response, 'assistant');
    }

    /**
     * 设置消息
     * @param string $message 消息
     * @param string $role 消息角色
     * @param array|null $extra 扩展信息
     * @param bool $overload
     * @return $this
     */
    public function withMessage(string $message, string $role = 'user', ?array $extra = null,
                                bool   $overload = false): static
    {
        $message = [
            'role'    => $role,
            'content' => $message,
        ];
        if ($extra) {
            $message = array_merge($message, $extra);
        }
        if ($overload) {
            $this->payload['messages'] = [];
        }
        $this->payload['messages'][] = $message;
        return $this;
    }

    /**
     * 获取密钥
     * @return Key
     */
    public function getKeyModel(): Key
    {
        if (empty($this->keyModel)) {
            if (!$keyModel = $this->channel->oneKey()) {
                throw new CompletionException(
                    sprintf('未配置[%s]接口密钥', $this->channel->getAttr('name'))
                );
            }
            $this->keyModel = $keyModel;
        }
        return $this->keyModel;
    }

    /**
     * @inheritdoc
     */
    public function completion(): Completion
    {
        try {
            $response = $this->getResponse();
            $data = $response->getBody()->getContents();
            //转换成array
            $data = json_decode($data, true);
            return $this->getCompletionStruct($data);
        } catch (CompletionException $e) {
            return $e->getCompletion();
        }
    }

    /**
     * @inheritdoc
     */
    public function completionStream(): Generator
    {
        $response = $this->getResponse(true);
        $stream = $response->getBody()->detach();
        stream_set_blocking($stream, 0); // 设置流为非阻塞模式

        $buffer = '';
        while (!feof($stream)) {
            $chunk = fread($stream, 1024);
            if (empty($chunk)) {
                usleep(100000); // 如果没有数据可读，则暂停 100 毫秒
                continue;
            }
            $buffer .= $chunk;

            if (!str_contains($buffer, "\n") && str_starts_with($buffer, '{')) {
                if ($content = json_decode($buffer, true)) {
                    $struct = $this->getCompletionStruct($content, true);
                    if (!empty($struct)) yield $struct;
                }
            } else {
                while (($pos = strpos($buffer, "\n")) !== false) {

                    $line = substr($buffer, 0, $pos);
                    $buffer = substr($buffer, $pos + 1);

                    $resp = '';
                    if (str_starts_with($line, 'data:')) {
                        $resp = str_replace("\n", '', trim(substr($line, 5)));
                    } else if (substr($line, 0, 1 == '{')) {
                        $resp = $line;
                    }
                    if (!$content = json_decode($resp, true)) continue;

                    $struct = $this->getCompletionStruct($content, true);
                    if (!empty($struct)) yield $struct;
                }
            }
        }
    }

    /**
     * 获取密钥
     * @return array
     */
    protected function getKeySecret(): array
    {
        return [
            'key'    => $this->getKeyModel()->getAttr('key'),
            'secret' => $this->getKeyModel()->getAttr('secret'),
        ];
    }

    /**
     * 获取请求参数
     * @return array
     */
    protected function getPayload(): array
    {
        $payload = $this->payload;
        $messages = $payload['messages'] ?? null;
        if (empty($messages)) {
            throw new CompletionException('未提供消息内容');
        }
        //取最后一条信息
        $message = $messages[count($messages) - 1];
        if ($message['role'] !== 'user') {
            throw new CompletionException('未提供用户消息');
        }
        //设置系统信息
        if ($this->system) {
            //添加到头部
            array_unshift($messages, [
                'role'    => 'system',
                'content' => $this->system
            ]);
            $payload['messages'] = $messages;
        }
        //设置流式回答
        if ($this->isStream) {
            $payload['stream'] = true;
        }

        return $payload;
    }

    /**
     * 设置请求配置
     * @param array $config
     * @param bool $override
     * @return void
     */
    protected function setPayload(array $config, bool $override = false): void
    {
        //去掉模型参数里的类前缀 如"XunfeiSpark:general"里的"XunfeiSpark:"
        if (str_contains($config['model'], ':')) {
            $config['model'] = substr($config['model'], strlen(class_basename($this)) + 1);
        }
        $this->config = $config;

        //覆盖掉配置
        if ($override) $this->payload = [];

        //设置模型
        if (!empty($config['model'])) {
            $this->withModel($config['model']);
        }
    }

    /**
     * 获取回答请求
     * @param bool $isStream 是否流式
     * @param array $clientOptions 客户端配置
     * @return ResponseInterface
     */
    protected function getResponse(bool $isStream = false, array $clientOptions = []): ResponseInterface
    {
        $options = [
            'json'   => $this->withStream($isStream)->getPayload(),
            'stream' => $isStream,
        ];
        if (!empty($clientOptions)) {
            $options = array_merge($options, $clientOptions);
        }
        try {
            $client = new Client();
            return $client->post($this->getCompletionsUrl(), $options);
        } catch (ClientException $e) {
            $content = $e->getResponse()?->getBody()->getContents();
            $exception = new CompletionException($content ?? $e->getMessage(), $e->getCode());
            //检测是不是json
            if ($data = json_decode($content, true)) {
                $exception->setData($data);
            }
            throw $exception;
        } catch (GuzzleException $e) {
            throw new CompletionException(
                $e->getCode() === 500 ? '内部错误' : $e->getMessage(), $e->getCode()
            );
        }
    }

    /**
     * 对话请求地址
     * @return string
     */
    protected function getCompletionsUrl(): string
    {
        return $this->baseUrl;
    }

    /**
     * AI回答结构
     * @param array $data
     * @param bool $isStream
     * @return Completion|null
     */
    abstract protected function getCompletionStruct(array $data, bool $isStream = false): ?CompletionInterface;

}